//
// CrackContext
//
// Copyright (C) jextra.net.
//
//  This file is part of the Crack build system.
//
//  The Crack build system is free software; you can redistribute it 
//  and/or modify it under the terms of the GNU Lesser General Public
//  License as published by the Free Software Foundation; either
//  version 2.1 of the License, or (at your option) any later version.
//
//  The Crack build system is distributed in the hope that it will be 
//  useful, but WITHOUT ANY WARRANTY; without even the implied warranty of
//  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
//  Lesser General Public License for more details.
//
//  You should have received a copy of the GNU Lesser General Public
//  License along with the Crack build system; if not, write to the Free
//  Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA
//  02111-1307 USA.
//

package net.jextra.crack;

import java.io.*;
import java.util.*;
import java.util.jar.*;
import java.util.jar.Attributes;
import net.jextra.crack.core.*;
import net.jextra.crack.updater.*;
import org.xml.sax.*;

/**
 * <p>
 * A number of static methods that return "global" environment information for use in build files,
 * etc.
 * </p><p>
 * This is most likely accessed via {@CrackBuild#getCrackContext}.
 * </p>
 */
public class CrackContext
{
    // ============================================================
    // Fields
    // ============================================================

    public static final String CRACK_HOME = "CRACK_HOME";

    public static final String DIR_CONF_LOCAL = "conf-local/";
    public static final String DIR_CONF = "conf/";
    public static final String DIR_WORK = "work/";
    public static final String DIR_CLASSES = "target/classes/";
    public static final String DIR_LIB = "lib/";

    public static final String FILENAME_CRACK_CONF = "crack-conf.xml";
    public static final String FILENAME_VERSION_PROPERTIES = "version.properties";

    public static final String SUFFIX_JAR = ".jar";

    public static final String PROP_VERSION_NUMBER = "version.number";

    private static CrackContext instance;

    private File buildFile;
    private CrackClassLoader classLoader;
    private File crackHome;
    private CrackConfReader confReader;

    // ============================================================
    // Constructors
    // ============================================================

    private CrackContext()
    {
        try
        {
            classLoader = new CrackClassLoader();
            for ( File jarFile : getCrackClasspath() )
            {
                classLoader.addPath( jarFile );
            }
        }
        catch ( Exception ex )
        {
            throw new RuntimeException( ex );
        }
    }

    // ============================================================
    // Methods
    // ============================================================

    // ----------
    // public
    // ----------

    public static CrackContext getInstance()
    {
        if ( instance == null )
        {
            instance = new CrackContext();
        }

        return instance;
    }

    public void setBuildFile( File buildFile )
    {
        this.buildFile = buildFile;
    }

    public File getBuildFile()
    {
        return buildFile;
    }

    public File getBuildDir()
    {
        return buildFile.getParentFile();
    }

    public CrackClassLoader getCrackClassLoader()
    {
        return classLoader;
    }

    /**
     * Read cracks version.properties files from the classpath and parse the version.number out of
     * it.
     */
    public String getCrackVersion()
        throws IOException
    {
        Properties props = new Properties();
        props.load( CrackContext.class.getResourceAsStream( FILENAME_VERSION_PROPERTIES ) );

        return props.getProperty( PROP_VERSION_NUMBER );
    }

    /**
     * Get system property, and if that does not exist, get environment variable with same name.
     */
    public String getSystemProperty( String name )
    {
        String value = System.getProperty( name );

        if ( value != null )
        {
            return value;
        }

        return System.getenv( name );
    }

    /**
     * Get the "home" directory where Crack is installed.
     */
    public File getCrackHome()
    {
        if ( crackHome == null )
        {
            String prop = getSystemProperty( CRACK_HOME );
            crackHome = prop == null ? null : new File( prop );
        }

        return crackHome;
    }

    /**
     * Usually $CRACK_HOME/conf-local.
     */
    public File getConfLocalDir()
    {
        return new File( getCrackHome(), DIR_CONF_LOCAL );
    }

    /**
     * Usually $CRACK_HOME/conf.
     */
    public File getConfDir()
    {
        return new File( getCrackHome(), DIR_CONF );
    }

    public File locateConfigFile( String fileName )
    {
        File file = new File( getConfLocalDir(), fileName );
        if ( file.exists() )
        {
            return file;
        }

        file = new File( getConfDir(), fileName );
        if ( file.exists() )
        {
            return file;
        }

        throw new RuntimeException( "Unable to locate conf file [" + fileName + "]" );
    }

    /**
     * Usually $CRACK_HOME/classes.
     */
    public File getClassesDir()
    {
        return new File( getCrackHome(), DIR_CLASSES );
    }

    /**
     * Usually $CRACK_HOME/lib.
     */
    public File getLibDir()
    {
        return new File( getCrackHome(), DIR_LIB );
    }

    /**
     * Usually $CRACK_HOME/work.
     */
    public File getWorkDir()
    {
        File dir = new File( getCrackHome(), DIR_WORK );
        dir.mkdir();
        return dir;
    }

    public CrackUpdater getUpdater()
        throws SAXException, IOException
    {
        return getCrackConfReader().getUpdater();
    }

    public Collection<File> getLibJars()
        throws Exception
    {
        LinkedHashSet<File> jarFiles = new LinkedHashSet<File>();

        searchForFiles( getLibDir(), SUFFIX_JAR, jarFiles );

        return jarFiles;
    }

    public File getCrackJar()
        throws Exception
    {
        return new File( getLibDir(), "crack.jar" );
    }

    public Collection<File> getCrackClasspath()
        throws Exception
    {
        LinkedHashSet<File> files = new LinkedHashSet<File>();

        //
        // Add classes dir to the classpath.
        //
        File classesDir = getClassesDir();
        if ( classesDir.exists() )
        {
            files.add( classesDir );
        }

        //
        // Also add tools.jar.
        //
        String jdk = System.getenv( "JAVA_HOME" );
        files.add( new File( jdk, "lib/tools.jar" ) );

        //
        // Add all of the jars from lib directory.
        //
        files.addAll( getLibJars() );

        return files;
    }

    /**
     * Parse through all of the installed plugins and return a Map of the plugin name versus
     * version number.
     */
    public Map<String, String> getPluginVersions()
        throws Exception
    {
        HashMap<String, String> versions = new HashMap<String, String>();

        for ( File jarFile : getLibJars() )
        {
            JarInputStream in = new JarInputStream( new FileInputStream( jarFile ) );
            Manifest man = in.getManifest();

            String name = man.getMainAttributes().getValue( Attributes.Name.IMPLEMENTATION_TITLE );
            if ( name == null )
            {
                name = jarFile.getName();
            }

            String version = man.getMainAttributes().getValue( Attributes.Name.IMPLEMENTATION_VERSION );

            if ( version != null )
            {
                versions.put( name, version );
            }

            in.close();
        }

        return versions;
    }

    // ----------
    // private
    // ----------

    private CrackConfReader getCrackConfReader()
    {
        try
        {
            if ( confReader == null )
            {
                confReader = new CrackConfReader();
                FileInputStream in = new FileInputStream( locateConfigFile( FILENAME_CRACK_CONF ) );
                confReader.parse( in );
                in.close();
            }

            return confReader;
        }
        catch ( Exception ex )
        {
            throw new CrackError( ex );
        }
    }

    private void searchForFiles( File file, String suffix, Set<File> files )
    {
        if ( file.isDirectory() )
        {
            for ( File child : file.listFiles( new SuffixFilter( suffix ) ) )
            {
                searchForFiles( child, suffix, files );
            }
        }
        else
        {
            files.add( file );
        }
    }

    // ============================================================
    // Inner Classes
    // ============================================================

    private static class SuffixFilter implements FilenameFilter
    {
        private String _suffix;

        public SuffixFilter( String suffix )
        {
            _suffix = suffix;
        }

        @Override
        public boolean accept( File dir, String name )
        {
            File file = new File( dir, name );
            if ( file.isDirectory() )
            {
                return true;
            }

            return name.endsWith( _suffix );
        }
    }
}
